/**
 * Hashing.java Created Aug 20, 2008 by Andrew Butler, PSL
 */
package prisms.arch.ds;

import org.json.simple.JSONArray;
import org.json.simple.JSONObject;

/**
 * Represents hashing data to be used for encryption and/or validation in PRISMS
 */
public class Hashing implements Cloneable
{
	private static String BASE_64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

	private long [] thePrimaryMultiples;

	private long [] thePrimaryModulos;

	private long [] theSecondaryMultiples;

	private long [] theSecondaryModulos;

	private int theMaxKeyLength;

	/**
	 * Creates a hashing object
	 */
	public Hashing()
	{
		theMaxKeyLength = 448 / 8;
	}

	/**
	 * @return The primary set of multiples used to hash a password
	 */
	public long [] getPrimaryMultiples()
	{
		return thePrimaryMultiples;
	}

	/**
	 * @return The primary set of modulos used to hash a password
	 */
	public long [] getPrimaryModulos()
	{
		return thePrimaryMultiples;
	}

	/**
	 * Sets the primary hashing data of this data object
	 * 
	 * @param multiples The primary multiples
	 * @param modulos The primary modulos
	 */
	public void setPrimaryHashing(long [] multiples, long [] modulos)
	{
		if(multiples.length != modulos.length)
			throw new IllegalArgumentException("Multiples and modulos must be the same length");
		for(int i = 0; i < multiples.length; i++)
		{
			if(multiples[i] < 1)
				throw new IllegalArgumentException("Multiples cannot be zero or negative");
			if(modulos[i] < 2)
				throw new IllegalArgumentException("Multiples cannot be one, zero or negative");
		}
		thePrimaryMultiples = multiples;
		thePrimaryModulos = modulos;
	}

	/**
	 * @return The secondary set of multiples used to further hash a primarily-hashed password
	 */
	public long [] getSecondaryMultiples()
	{
		return theSecondaryMultiples;
	}

	/**
	 * @return The secondary set of modulos used to further hash a primarily-hashed password
	 */
	public long [] getSecondaryModulos()
	{
		return theSecondaryMultiples;
	}

	/**
	 * Sets the secondary hashing data for this data object
	 * 
	 * @param multiples The secondary multiples
	 * @param modulos The secondary modulos
	 */
	public void setSecondaryHashing(long [] multiples, long [] modulos)
	{
		if(multiples.length != modulos.length)
			throw new IllegalArgumentException("Multiples and modulos must be the same length");
		for(int i = 0; i < multiples.length; i++)
		{
			if(multiples[i] < 1)
				throw new IllegalArgumentException("Multiples cannot be zero or negative");
			if(modulos[i] < 2)
				throw new IllegalArgumentException("Multiples cannot be one, zero or negative");
		}
		theSecondaryMultiples = multiples;
		theSecondaryModulos = modulos;
	}

	/**
	 * Fills this data object's primary hashing data randomly
	 * 
	 * @param length The number of elements to put in the primary data fields. A greater number is
	 *        more secure but more costly performance-wise
	 */
	public void randomlyFillPrimary(int length)
	{
		long [][] hashing = generateHashing(length);
		thePrimaryMultiples = hashing[0];
		thePrimaryModulos = hashing[1];
	}

	/**
	 * Fills this data object's secondary hashing data randomly
	 * 
	 * @param length The number of elements to put in the secondary data fields. A greater number is
	 *        more secure but more costly performance-wise
	 */
	public void randomlyFillSecondary(int length)
	{
		long [][] hashing = generateHashing(length);
		theSecondaryMultiples = hashing[0];
		theSecondaryModulos = hashing[1];
	}

	/**
	 * Performs the primary hashing on a plain-text password
	 * 
	 * @param password The plain-text password to hash
	 * @return Hashing data specific (but not necessarily unique) to the password
	 */
	public long [] partialHash(String password)
	{
		long [] ret = new long [thePrimaryModulos.length];
		for(int r = 0; r < ret.length; r++)
		{
			ret[r] = 0;
			for(int p = 0; p < password.length(); p++)
				ret[r] = ((ret[r] + password.charAt(p)) * thePrimaryMultiples[r])
					% thePrimaryModulos[r];
		}
		return ret;
	}

	/**
	 * Uses this hashing's secondary hash values to generate a unique key from a partially hashed
	 * password
	 * 
	 * @param partialHash A partially hashed password, generated by {@link #partialHash(String)} or
	 *        another method of identical implementation
	 * @return A key that is unique for the a user's password and this hashing data
	 */
	public String generateKey(long [] partialHash)
	{
		long [] hashed = new long [partialHash.length];
		for(int i = 0; i < partialHash.length; i++)
		{
			hashed[i] = partialHash[i];
			for(int j = 0; j < theSecondaryMultiples.length; j++)
				hashed[i] = (hashed[i] * theSecondaryMultiples[j]) % theSecondaryModulos[j];
		}
		String [] keyStringEls = new String [hashed.length];
		for(int h = 0; h < hashed.length; h++)
			keyStringEls[h] = toKeyString(hashed[h]);
		while(isTooBig(keyStringEls))
			keyStringEls = downsize(keyStringEls);
		String ret = "";
		for(int k = 0; k < keyStringEls.length; k++)
			ret += keyStringEls[k];
		return ret;
	}

	private boolean isTooBig(String [] keys)
	{
		int size = 0;
		for(int k = 0; k < keys.length; k++)
			size += keys[k].length() * 8;
		return size > theMaxKeyLength;
	}

	private String [] downsize(String [] keys)
	{
		// Cancels the highest 64-base digit in each element to reduce its size with minimal
		// complexity loss
		boolean allOnes = true;
		for(int k = 0; k < keys.length; k++)
			if(keys[k].length() > 1)
			{
				keys[k] = keys[k].substring(1);
				allOnes = false;
			}
		if(allOnes)
			keys = prisms.util.ArrayUtils.remove(keys, 0);
		return keys;
	}

	private static String toKeyString(long value)
	{
		String ret = "";
		while(value > 0)
		{
			ret += BASE_64.charAt((int) (value % 64));
			value /= 64;
		}
		return ret;
	}

	private static long [][] generateHashing(int length)
	{
		long [][] ret = new long [2] [length];
		for(int i = 0; i < ret[0].length; i++)
		{
			do
			{
				ret[0][i] = (long) (0x800000L * Math.random());
				if(ret[0][i] % 2 == 0)
					ret[0][i]++;
			} while(ret[0][i] < 0x100000L);
			do
			{
				ret[1][i] = (long) (0x80000000L * Math.random());
			} while(ret[1][i] < 0x10000000L);
		}
		return ret;
	}

	/**
	 * Serializes this hashing data object into a form that may be passed over a network
	 * 
	 * @return A JSON-formatted representation of this hashing data object
	 */
	public JSONObject toJson()
	{
		JSONObject ret = new JSONObject();
		JSONArray array;

		array = new JSONArray();
		for(int i = 0; i < thePrimaryMultiples.length; i++)
			array.add(new Long(thePrimaryMultiples[i]));
		ret.put("primaryMultiples", array);

		array = new JSONArray();
		for(int i = 0; i < thePrimaryModulos.length; i++)
			array.add(new Long(thePrimaryModulos[i]));
		ret.put("primaryModulos", array);

		array = new JSONArray();
		for(int i = 0; i < theSecondaryMultiples.length; i++)
			array.add(new Long(theSecondaryMultiples[i]));
		ret.put("secondaryMultiples", array);

		array = new JSONArray();
		for(int i = 0; i < theSecondaryModulos.length; i++)
			array.add(new Long(theSecondaryModulos[i]));
		ret.put("secondaryModulos", array);

		return ret;
	}

	/**
	 * Deserializes a Hashing object from its JSON representation
	 * 
	 * @param json The JSONObject representing a Hashing object
	 * @return The Hashing object represented by the JSON
	 */
	public static Hashing fromJson(JSONObject json)
	{
		Hashing ret = new Hashing();

		JSONArray pMults = (JSONArray) json.get("primaryMultiples");
		ret.thePrimaryMultiples = new long [pMults.size()];
		for(int i = 0; i < ret.thePrimaryMultiples.length; i++)
			ret.thePrimaryMultiples[i] = ((Number) pMults.get(i)).longValue();

		JSONArray pMods = (JSONArray) json.get("primaryModulos");
		ret.thePrimaryModulos = new long [pMods.size()];
		for(int i = 0; i < ret.thePrimaryModulos.length; i++)
			ret.thePrimaryModulos[i] = ((Number) pMods.get(i)).longValue();

		JSONArray sMults = (JSONArray) json.get("secondaryMultiples");
		ret.theSecondaryMultiples = new long [sMults.size()];
		for(int i = 0; i < ret.theSecondaryMultiples.length; i++)
			ret.theSecondaryMultiples[i] = ((Number) sMults.get(i)).longValue();

		JSONArray sMods = (JSONArray) json.get("secondaryModulos");
		ret.theSecondaryModulos = new long [sMods.size()];
		for(int i = 0; i < ret.theSecondaryModulos.length; i++)
			ret.theSecondaryModulos[i] = ((Number) sMods.get(i)).longValue();

		return ret;
	}

	public Hashing clone()
	{
		Hashing ret;
		try
		{
			ret = (Hashing) super.clone();
		} catch(CloneNotSupportedException e)
		{
			throw new IllegalStateException(e);
		}
		return ret;
	}
}
